using System;
using System.Collections.Generic;
using System.Text;
using System.Reflection;
using System.Resources;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.IO;

using ClassBuilderWizard.Interfaces;

using Microsoft.CSharp;
using Microsoft.VisualBasic;

namespace ClassBuilderWizard
{
    internal class EntityBuilder
    {
        private DatabaseObject _DbObject;
        private ManualObject _ManualObject;
        private Parameters _Parameters;

        private StringBuilder _Fields;
        private StringBuilder _Properties;
        private StringBuilder _Inits;

        private CodeNamespace _CurrentNamespace;

        #region Initialization Code
        public EntityBuilder(DatabaseObject dbo, Parameters pars)
        {
            this._DbObject = dbo;
            this._ManualObject = null;
            this._Parameters = pars;

            InitContent();
            ExecuteBuilder();
        }
        public EntityBuilder(ManualObject props, Parameters pars)
        {
            this._ManualObject = props;
            this._DbObject = null;
            this._Parameters = pars;

            InitContent();
            ExecuteBuilder();
        }

        private void InitContent()
        {
            this._Fields = new StringBuilder();
            this._Properties = new StringBuilder();
            this._Inits = new StringBuilder();
        }
        #endregion

        #region Code Generation Methods
        private CodeNamespace GenerateNamespace()
        {
            //init the namespace
            CodeNamespace ns = new CodeNamespace(this._Parameters.ProjectNamespace);
            //add some imports
            ns.Imports.Add(new CodeNamespaceImport("System"));
            ns.Imports.Add(new CodeNamespaceImport("System.Xml.Serialization"));

            return ns;
        }
        private CodeTypeDeclaration GenerateClassDef()
        {
            //create the class def
            CodeTypeDeclaration typeDec = new CodeTypeDeclaration(this._Parameters.NewItemName);
            typeDec.IsClass = true;
            typeDec.Attributes = MemberAttributes.Public;

            CodeAttributeDeclaration serializeable = new CodeAttributeDeclaration("Serializable");

            CodeAttributeDeclaration xmlroot = new CodeAttributeDeclaration("XmlRoot"); //new CodeTypeReference(typeof(System.Xml.Serialization.XmlRootAttribute)));
            xmlroot.Arguments.Add(new CodeAttributeArgument(new CodePrimitiveExpression(this._Parameters.NewItemName)));

            typeDec.CustomAttributes.Add(serializeable);
            typeDec.CustomAttributes.Add(xmlroot);

            return typeDec;
        }
        private CodeMemberField GenerateField(Type t, string name)
        {
            CodeTypeReference ctr = new CodeTypeReference(t);
            CodeMemberField cmf = new CodeMemberField(ctr, name);
            cmf.Attributes = MemberAttributes.Private;

            return cmf;
        }
        private CodeConstructor GenerateInitMethod()
        {
            CodeConstructor cc = new CodeConstructor();
            cc.Attributes = MemberAttributes.Public | MemberAttributes.Final;

            return cc;
        }

        private string GenerateFieldName(string name)
        {
            return ((this._Parameters.CurrentProjectType == ProjectType.VB) ? "m_" : "_") + name;
        }

        private CodeMemberProperty GeneratePublicProperty(Type t, string name)
        {
            CodeMemberProperty cmp = new CodeMemberProperty();
            cmp.Attributes = MemberAttributes.Public | MemberAttributes.Final;
            cmp.Name = name;
            cmp.HasGet = true;
            cmp.HasSet = true;
            
            CodeTypeReference ctr = new CodeTypeReference(t);
            cmp.Type = ctr;

            string field = GenerateFieldName(name);

            //setup the get
            CodeThisReferenceExpression thisRef = new CodeThisReferenceExpression();
            CodeFieldReferenceExpression fld = new CodeFieldReferenceExpression(thisRef, field);
            CodeMethodReturnStatement get = new CodeMethodReturnStatement(fld);
            cmp.GetStatements.Add(get);

            //setup the set
            CodeArgumentReferenceExpression setVal = new CodeArgumentReferenceExpression("value");
            CodeAssignStatement set = new CodeAssignStatement(fld, setVal);
            cmp.SetStatements.Add(set);

            return cmp;
        }

        private CodeAssignStatement GenerateFieldInit(string fieldName, object value, Type fieldType)
        {
            CodeAssignStatement cas;
            CodeThisReferenceExpression ctre = new CodeThisReferenceExpression();
            if (!fieldType.IsArray && fieldType != typeof(Guid))
            {
                CodePrimitiveExpression newVal = new CodePrimitiveExpression(value);
                cas = new CodeAssignStatement(new CodeFieldReferenceExpression(ctre, fieldName), newVal);
            }
            else if (!fieldType.IsArray && fieldType == typeof(Guid))
            {
                CodeObjectCreateExpression newVal = new CodeObjectCreateExpression(fieldType, new CodeExpression[] { new CodePrimitiveExpression(value) });
                cas = new CodeAssignStatement(new CodeFieldReferenceExpression(ctre, fieldName), newVal);
            }
            else
            {
                CodeArrayCreateExpression newVal = new CodeArrayCreateExpression(value.GetType(), 0);
                cas = new CodeAssignStatement(new CodeFieldReferenceExpression(ctre, fieldName), newVal);
            }

            return cas;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns>
        /// public void Fill(SqlDataReader dr)
        /// {
        ///     PropertyDescriptorCollection props = TypeDescriptor.GetProperties(this);
        ///     for (int i = 0; i < props.Count; i++)
        ///     {
        ///         PropertyDescriptor prop = props[i];
        ///         if (!prop.IsReadOnly)
        ///         {
        ///            try
        ///            {
        ///                 if (!dr[prop.Name].Equals(DBNull.Value))
        ///                 {
        ///                     prop.SetValue(this, Convert.ChangeType(dr[prop.Name], prop.GetValue(this).GetType()));
        ///                 }
        ///             }
        ///             catch { }
        ///         }
        ///     }
        /// }
        /// </returns>
        private CodeMemberMethod GenerateFillFunction(Type parameterType)
        {
            CodeMemberMethod func = new CodeMemberMethod();
            func.Name = "Fill";
            func.Attributes = MemberAttributes.Public | MemberAttributes.Final;
            func.ReturnType = new CodeTypeReference(typeof(void));
            func.Parameters.Add(new CodeParameterDeclarationExpression(parameterType, "dr"));
            CodeArgumentReferenceExpression dr = new CodeArgumentReferenceExpression("dr");

            //PropertyDescriptorCollection props = 
            CodeVariableDeclarationStatement propsInit = new CodeVariableDeclarationStatement(typeof(System.ComponentModel.PropertyDescriptorCollection), "props");
            
            //TypeDescriptor.GetProperties(this);
            CodeThisReferenceExpression thisRef = new CodeThisReferenceExpression();
            CodeTypeReferenceExpression typeDescriptor = new CodeTypeReferenceExpression(typeof(System.ComponentModel.TypeDescriptor));
            CodeMethodReferenceExpression getProperties = new CodeMethodReferenceExpression(typeDescriptor, "GetProperties");
            CodeMethodInvokeExpression getPropertiesInvoke = new CodeMethodInvokeExpression(getProperties, new CodeExpression[] { thisRef });
            propsInit.InitExpression = getPropertiesInvoke;
            CodeVariableReferenceExpression props = new CodeVariableReferenceExpression("props");

            //int i = 
            CodeVariableDeclarationStatement iInit = new CodeVariableDeclarationStatement(typeof(int), "i");
            //0;
            CodePrimitiveExpression zero = new CodePrimitiveExpression(0);
            iInit.InitExpression = zero;
            CodeVariableReferenceExpression i = new CodeVariableReferenceExpression("i");

            //props.Count
            CodePropertyReferenceExpression propsDotCount = new CodePropertyReferenceExpression(props, "Count");
            CodeBinaryOperatorExpression iLessThanCount = new CodeBinaryOperatorExpression(i, CodeBinaryOperatorType.LessThan, propsDotCount);

            //i + 1
            CodeBinaryOperatorExpression iPlusOne = new CodeBinaryOperatorExpression(i, CodeBinaryOperatorType.Add, new CodePrimitiveExpression(1));
            //i = i + 1
            CodeAssignStatement iEqualsiPlusOne = new CodeAssignStatement(i, iPlusOne);

            //PropertyDescriptor prop = 
            CodeVariableDeclarationStatement propInit = new CodeVariableDeclarationStatement(typeof(System.ComponentModel.PropertyDescriptor), "prop");
            //props[i];
            CodeIndexerExpression propsIndexer = new CodeIndexerExpression(props, new CodeExpression[] { i });
            propInit.InitExpression = propsIndexer;
            CodeVariableReferenceExpression prop = new CodeVariableReferenceExpression("prop");

            //prop.Name
            CodePropertyReferenceExpression propDotName = new CodePropertyReferenceExpression(prop, "Name");
            //dr[prop.Name]
            CodeIndexerExpression drIndexerByPropName = new CodeIndexerExpression(dr, new CodeExpression[] { propDotName });
            //DBNull.Value
            CodePropertyReferenceExpression dBNullDotValue = new CodePropertyReferenceExpression(new CodeTypeReferenceExpression(typeof(System.DBNull)), "Value");

            CodeMethodReferenceExpression drIndexerByPropNameEquals = new CodeMethodReferenceExpression(drIndexerByPropName, "Equals");
            CodeMethodInvokeExpression drIndexerByPropNameEqualsDbNull = new CodeMethodInvokeExpression(drIndexerByPropNameEquals, new CodeExpression[] { dBNullDotValue });

            //prop.GetValue(this).GetType()
            CodeMethodInvokeExpression propDotGetValue = new CodeMethodInvokeExpression(prop, "GetValue", new CodeExpression[] { thisRef });
            CodeMethodInvokeExpression propDotGetValueDotGetType = new CodeMethodInvokeExpression(propDotGetValue, "GetType", new CodeExpression[] { });

            //Convert.ChangeType(dr[prop.Name], prop.GetValue(this).GetType())
            CodeMethodInvokeExpression castdrVal = new CodeMethodInvokeExpression(new CodeTypeReferenceExpression(typeof(System.Convert)), "ChangeType", new CodeExpression[] { drIndexerByPropName, propDotGetValueDotGetType });

            //prop.SetValue()
            CodeMethodInvokeExpression propDotSetValueTodrIndexerByPropName = new CodeMethodInvokeExpression(prop, "SetValue", new CodeExpression[] { thisRef, castdrVal });
            CodeExpressionStatement callSetValueStatement = new CodeExpressionStatement(propDotSetValueTodrIndexerByPropName);

            //dr[prop.Name].Equals(DBNull.Value) != true
            CodeBinaryOperatorExpression drValNotEqualToDBNull = new CodeBinaryOperatorExpression(drIndexerByPropNameEqualsDbNull, CodeBinaryOperatorType.IdentityInequality, new CodePrimitiveExpression(true));

            //if (!dr[prop.Name].Equals(DBNull.Value))
            CodeConditionStatement ifdrIndexerIsNotDbNull = new CodeConditionStatement(drValNotEqualToDBNull, new CodeStatement[] { callSetValueStatement });

            CodeCatchClause catchBlock = new CodeCatchClause();
            //try{} catch{} finally{}
            CodeTryCatchFinallyStatement tryBlock = new CodeTryCatchFinallyStatement(new CodeStatement[] { ifdrIndexerIsNotDbNull }, new CodeCatchClause[] { catchBlock });

            //prop.IsReadOnly
            CodePropertyReferenceExpression propDotIsReadOnly = new CodePropertyReferenceExpression(prop, "IsReadOnly");
            CodeBinaryOperatorExpression notpropDotIsReadOnly = new CodeBinaryOperatorExpression(propDotIsReadOnly, CodeBinaryOperatorType.IdentityInequality, new CodePrimitiveExpression(true));
            //if (!prop.IsReadOnly)
            CodeConditionStatement ifNotReadOnly = new CodeConditionStatement(notpropDotIsReadOnly, new CodeStatement[] { tryBlock });

            CodeIterationStatement forEachProp = new CodeIterationStatement(iInit, iLessThanCount, iEqualsiPlusOne, new CodeStatement[] { propInit, ifNotReadOnly });

            func.Statements.Add(propsInit);
            func.Statements.Add(forEachProp);

            return func;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns>
        ///         public AllTypes FromXmlString(string xml)
        ///         {
        ///             XmlSerializer xs = new XmlSerializer(this.GetType());
        ///             XmlTextReader xtr = new XmlTextReader(new StringReader(xml));
        /// 
        ///             return (AllTypes)xs.Deserialize(xtr);
        ///         }
        /// </returns>
        private CodeMemberMethod GenerateFromXmlStringFunction()
        {
            CodeMemberMethod func = new CodeMemberMethod();
            func.Name = "FromXmlString";
            func.Attributes = MemberAttributes.Public | MemberAttributes.Final;
            func.ReturnType = new CodeTypeReference(this._Parameters.NewItemName);
            func.Parameters.Add(new CodeParameterDeclarationExpression(typeof(string), "xml"));

            CodeVariableDeclarationStatement xsInit = new CodeVariableDeclarationStatement(typeof(System.Xml.Serialization.XmlSerializer), "xs");
            CodeThisReferenceExpression thisRef = new CodeThisReferenceExpression();
            CodeMethodReferenceExpression thisGetType = new CodeMethodReferenceExpression(thisRef, "GetType");
            CodeMethodInvokeExpression thisGetTypeInvoke = new CodeMethodInvokeExpression();
            thisGetTypeInvoke.Method = thisGetType;

            CodeObjectCreateExpression xsNew = new CodeObjectCreateExpression(typeof(System.Xml.Serialization.XmlSerializer), new CodeExpression[] { thisGetTypeInvoke });
            xsInit.InitExpression = xsNew;

            CodeVariableReferenceExpression xs = new CodeVariableReferenceExpression("xs");

            CodeArgumentReferenceExpression xml = new CodeArgumentReferenceExpression("xml");
            CodeObjectCreateExpression newStringReader = new CodeObjectCreateExpression(typeof(StringReader), new CodeExpression[] { xml });
            CodeObjectCreateExpression newXtr = new CodeObjectCreateExpression(typeof(System.Xml.XmlTextReader), new CodeExpression[] { newStringReader });
            CodeVariableDeclarationStatement xtrInit = new CodeVariableDeclarationStatement(typeof(System.Xml.XmlTextReader), "xtr");
            xtrInit.InitExpression = newXtr;

            CodeVariableReferenceExpression xtr = new CodeVariableReferenceExpression("xtr");

            CodeMethodReferenceExpression xsDotDeserialize = new CodeMethodReferenceExpression(xs, "Deserialize");
            CodeMethodInvokeExpression xsDotDeserializeInvoke = new CodeMethodInvokeExpression(xsDotDeserialize, new CodeExpression[] { xtr });

            CodeCastExpression cast = new CodeCastExpression(new CodeTypeReference(this._Parameters.NewItemName), xsDotDeserializeInvoke);
            CodeMethodReturnStatement ret = new CodeMethodReturnStatement(cast);

            func.Statements.Add(xsInit);
            func.Statements.Add(xtrInit);
            func.Statements.Add(ret);

            return func;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns>
        /// public string ToXmlString()
        /// {
        ///     XmlSerializer oSerialize = new XmlSerializer(this.GetType());
        ///     MemoryStream oBuffer = new MemoryStream();
        ///     oSerialize.Serialize(oBuffer, this);
        ///     string returnVal = ASCIIEncoding.ASCII.GetString(oBuffer.ToArray());
        ///
        ///     return returnVal;
        /// }
        /// </returns>
        private CodeMemberMethod GenerateToXmlStringFunction()
        {
            CodeMemberMethod func = new CodeMemberMethod();
            func.Name = "ToXmlString";
            func.Attributes = MemberAttributes.Public | MemberAttributes.Final;
            func.ReturnType = new CodeTypeReference(typeof(string));

            CodeVariableDeclarationStatement oSerializeInit = new CodeVariableDeclarationStatement(typeof(System.Xml.Serialization.XmlSerializer), "oSerialize");
            CodeThisReferenceExpression thisRef = new CodeThisReferenceExpression();
            CodeMethodReferenceExpression thisGetType = new CodeMethodReferenceExpression(thisRef, "GetType");
            CodeMethodInvokeExpression thisGetTypeInvoke = new CodeMethodInvokeExpression();
            thisGetTypeInvoke.Method = thisGetType;

            CodeObjectCreateExpression oSerializeNew = new CodeObjectCreateExpression(typeof(System.Xml.Serialization.XmlSerializer), new CodeExpression[] { thisGetTypeInvoke });
            oSerializeInit.InitExpression = oSerializeNew;

            CodeVariableDeclarationStatement oBufferInit = new CodeVariableDeclarationStatement(typeof(System.IO.MemoryStream), "oBuffer");
            CodeObjectCreateExpression oBufferNew = new CodeObjectCreateExpression(typeof(System.IO.MemoryStream));
            oBufferInit.InitExpression = oBufferNew;

            CodeVariableReferenceExpression oSerialize = new CodeVariableReferenceExpression("oSerialize");
            CodeVariableReferenceExpression oBuffer = new CodeVariableReferenceExpression("oBuffer");

            CodeMethodInvokeExpression oSerializeDotSerialize = new CodeMethodInvokeExpression(oSerialize, "Serialize");
            oSerializeDotSerialize.Parameters.Add(oBuffer);
            oSerializeDotSerialize.Parameters.Add(thisRef);

            CodeMethodInvokeExpression oBufferDotToArray = new CodeMethodInvokeExpression(oBuffer, "ToArray");

            CodeVariableDeclarationStatement returnValInit = new CodeVariableDeclarationStatement(typeof(string), "returnVal");
            CodePropertyReferenceExpression ascii = new CodePropertyReferenceExpression(new CodeTypeReferenceExpression(typeof(System.Text.ASCIIEncoding)), "ASCII");
            CodeMethodReferenceExpression asciiDotGetString = new CodeMethodReferenceExpression(ascii, "GetString");
            CodeMethodInvokeExpression asciiDotGetStringInvoke = new CodeMethodInvokeExpression(asciiDotGetString, new CodeExpression[] { oBufferDotToArray });
            returnValInit.InitExpression = asciiDotGetStringInvoke;

            CodeVariableReferenceExpression returnVal = new CodeVariableReferenceExpression("returnVal");
            CodeMethodReturnStatement ret = new CodeMethodReturnStatement(returnVal);

            func.Statements.Add(oSerializeInit);
            func.Statements.Add(oBufferInit);
            func.Statements.Add(oSerializeDotSerialize);
            func.Statements.Add(returnValInit);
            func.Statements.Add(ret);

            return func;
        }
        /// <summary>
        /// Get a strongly typed default value for the type passed
        /// </summary>
        /// <param name="t"></param>
        /// <param name="lang"></param>
        /// <returns></returns>
        private object GetTypeDefaultValue(Type t)
        {
            //we really only need to initialize nullable types
            object result = null;
            if (t == typeof(String))
            {
                result = string.Empty;
            }
            else if (t == typeof(Byte[]))
            {
                result = new Byte[] { };
            }
            else if (t == typeof(Object))
            {
                result = new Object();
            }
            else if (t == typeof(Guid))
            {
                result = Guid.Empty.ToString();
            }

            return result;
        }
        /// <summary>
        /// Perform the final code generation
        /// </summary>
        /// <param name="prov"></param>
        /// <returns></returns>
        private string GenerateClassBody(CodeDomProvider prov)
        {
            CodeGeneratorOptions cgo = new CodeGeneratorOptions();
            cgo.BracingStyle = "C"; //"C" braces on next line -- other option "Block" is on the same line
            cgo.IndentString = "\t"; //tab for indents
            cgo.BlankLinesBetweenMembers = false;

            //setup the containers for the text of the code
            StringBuilder sb = new StringBuilder();
            StringWriter sw = new StringWriter(sb);

            //perform the generation
            prov.GenerateCodeFromNamespace(this._CurrentNamespace, sw, cgo);

            //return the text of the generated code
            return sb.ToString();
        }
        #endregion

        #region Internal Execution Methods
        private void ExecuteBuilder()
        {
            this._CurrentNamespace = GenerateNamespace();
            CodeTypeDeclaration cls = GenerateClassDef();
            this._CurrentNamespace.Types.Add(cls);

            CodeConstructor init = GenerateInitMethod();
            cls.Members.Add(init);

            if (this._DbObject != null)
                ExecuteFromDb(cls, init);
            else
                ExecuteFromManual(cls, init);

            cls.Members.Add(GenerateFillFunction(typeof(System.Data.SqlClient.SqlDataReader)));
            cls.Members.Add(GenerateFillFunction(typeof(System.Data.DataRow)));
            cls.Members.Add(GenerateToXmlStringFunction());
            cls.Members.Add(GenerateFromXmlStringFunction());
            if (this._DbObject != null && this._DbObject.EntityMembers != null && this._DbObject.EntityMembers.Count > 0)
                cls.Members.AddRange(this._DbObject.EntityMembers);
        }

        private void ExecuteFromDb(CodeTypeDeclaration cls, CodeConstructor init)
        {
            foreach (DatabaseColumn dbc in this._DbObject.Columns)
            {
                string fieldName = GenerateFieldName(dbc.Name);
                cls.Members.Add(GenerateField(dbc.CLRType, fieldName));

                object defaultVal = GetTypeDefaultValue(dbc.CLRType);
                if (defaultVal != null)
                {
                    //put init code here for field
                    init.Statements.Add(GenerateFieldInit(fieldName, defaultVal, dbc.CLRType));
                }

                //add a public property for the column
                cls.Members.Add(GeneratePublicProperty(dbc.CLRType, dbc.Name));
            }
        }
        private void ExecuteFromManual(CodeTypeDeclaration cls, CodeConstructor init)
        {
            foreach (ManualProperty mp in this._ManualObject.Properties)
            {
                Type t = (mp.CLRType == null) ? typeof(object) : mp.CLRType;
                string fieldName = GenerateFieldName(mp.Name);
                cls.Members.Add(GenerateField(t, fieldName));

                object defaultVal = GetTypeDefaultValue(t);

                if (defaultVal != null)
                {
                    //put init code in the constructor for the field
                    init.Statements.Add(GenerateFieldInit(fieldName, defaultVal, t));
                }

                //add a public property
                cls.Members.Add(GeneratePublicProperty(t, mp.Name));
            }
        }
        #endregion

        #region Public-facing methods
        public string GetEntityClassBody()
        {
            if (this._Parameters.CurrentProjectType == ProjectType.VB)
                return VBCode;
            else
                return CSharpCode;
        }
        #endregion

        #region Private Properties
        private string CSharpCode
        {
            get
            {
                CSharpCodeProvider provider = new CSharpCodeProvider();
                return GenerateClassBody(provider);
            }
        }
        private string VBCode
        {
            get
            {
                VBCodeProvider provider = new VBCodeProvider();
                return GenerateClassBody(provider);
            }
        }
        #endregion
    }
}
